package org.xx.armory.doc.impl;

import com.itextpdf.text.Anchor;
import com.itextpdf.text.BaseColor;
import com.itextpdf.text.Chunk;
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Element;
import com.itextpdf.text.Font;
import com.itextpdf.text.Image;
import com.itextpdf.text.Paragraph;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.PdfPCell;
import com.itextpdf.text.pdf.PdfPTable;
import com.itextpdf.text.pdf.PdfWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xx.armory.commons.ForLogging;
import org.xx.armory.doc.Breaker;
import org.xx.armory.doc.ElementProperties;
import org.xx.armory.doc.ElementSize;
import org.xx.armory.doc.HyperLink;
import org.xx.armory.doc.Img;
import org.xx.armory.doc.Inline;
import org.xx.armory.doc.PageBreaker;
import org.xx.armory.doc.Template;
import org.xx.armory.doc.TemplateContext;
import org.xx.armory.doc.Text;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

import static com.itextpdf.text.pdf.BaseFont.IDENTITY_H;
import static com.itextpdf.text.pdf.BaseFont.createFont;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.join;
import static org.apache.commons.lang3.StringUtils.startsWithIgnoreCase;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.xx.armory.commons.Validators.rejectIfBlank;
import static org.xx.armory.commons.Validators.rejectIfNull;
import static org.xx.armory.doc.ElementProperties.ALIGN;
import static org.xx.armory.doc.ElementProperties.COLOR;
import static org.xx.armory.doc.ElementProperties.FONT_FAMILY;
import static org.xx.armory.doc.ElementProperties.FONT_SIZE;
import static org.xx.armory.doc.ElementProperties.FONT_WEIGHT;
import static org.xx.armory.doc.ElementProperties.HEIGHT;
import static org.xx.armory.doc.ElementProperties.MARGIN_BOTTOM;
import static org.xx.armory.doc.ElementProperties.MARGIN_LEFT;
import static org.xx.armory.doc.ElementProperties.MARGIN_RIGHT;
import static org.xx.armory.doc.ElementProperties.MARGIN_TOP;
import static org.xx.armory.doc.ElementProperties.TEXT_ALIGN;
import static org.xx.armory.doc.ElementProperties.TEXT_DECORATION;
import static org.xx.armory.doc.ElementProperties.TEXT_INDENT;
import static org.xx.armory.doc.ElementProperties.WIDTH;
import static org.xx.armory.doc.ElementProperties.extend;

/**
 * PDF文档模板。
 *
 * @author Haart
 */
public class PdfTemplate
        implements Template, AutoCloseable {
    /**
     *
     */
    private static final Pattern PLACE_HOLDER = Pattern.compile(
            "(?<![${}]) # Leading chars.\n"
                    + "\\$\\{([a-z0-9_]+)} # Placeholder name.\n"
                    + "(?![${}]) # Tailing chars.\n",
            Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.COMMENTS);
    private static final String CLASS_PATH_PREFIX = "classpath:";
    private final String title;
    private final String author;
    private final String subject;
    private final String[] keywords;
    private final org.xx.armory.doc.Document doc;
    private final ElementProperties documentProperties;
    private final Map<String, ElementProperties> paragraphProperties;
    private final Map<String, ElementProperties> tableProperties;
    private final Map<String, ElementProperties> textProperties;

    @ForLogging
    private final Logger logger = LoggerFactory.getLogger(PdfTemplate.class);

    private int maxRepeatCount;
    private int repeatCount;

    /**
     * 构造PDF文档模板。
     *
     * @param doc
     *         文档。
     * @param title
     *         标题, 会被正规化。
     * @param author
     *         作者, 会被正规化。
     * @param subject
     *         主题, 会被正规化。
     * @param keywords
     *         关键字, 如果是{@code null}则看作长度为{@code 0}的数组。
     * @param documentProperties
     *         文档属性集合。
     * @param paragraphProperties
     *         段落元素默认属性集合。
     * @param tableProperties
     *         表格元素默认属性集合。
     * @param textProperties
     *         文本元素默认属性集合。
     * @throws NullPointerException
     *         如果参数{@code doc}是{@code null}。
     */
    public PdfTemplate(
            org.xx.armory.doc.Document doc,
            String title,
            String author,
            String subject,
            String[] keywords,
            ElementProperties documentProperties,
            Map<String, ElementProperties> paragraphProperties,
            Map<String, ElementProperties> tableProperties,
            Map<String, ElementProperties> textProperties
    ) {
        rejectIfNull(doc, "doc");

        this.doc = doc;
        this.title = trimToEmpty(title);
        this.author = trimToEmpty(author);
        this.subject = trimToEmpty(subject);
        this.keywords = keywords == null ? new String[0] : keywords;

        if (documentProperties == null) {
            documentProperties = new ElementProperties();
            documentProperties.put(FONT_SIZE, 9);
        }
        this.documentProperties = new ElementProperties(documentProperties);

        this.paragraphProperties = paragraphProperties != null ? new HashMap<>(paragraphProperties) : new HashMap<>();
        this.tableProperties = tableProperties != null ? new HashMap<>(tableProperties) : new HashMap<>();
        this.textProperties = textProperties != null ? new HashMap<>(textProperties) : new HashMap<>();

        this.repeatCount = 0;
        this.maxRepeatCount = 0;
    }

    /**
     * 根据样式类名找到对应的样式集合。
     *
     * @param map
     *         所有的样式集合对应表。
     * @param className
     *         样式类名，自动去除首尾空格，如果是{@code null}则看作空字符串。
     * @return 样式类名对应的样式集合。
     * @throws IllegalArgumentException
     *         如果参数{@code map}是{@code null}。
     */
    private static ElementProperties[] props(
            Map<String, ElementProperties> map,
            String className
    ) {
        className = trimToEmpty(className);

        final String[] classNames = className.isEmpty() ? new String[]{""} : className.split("\\s+");
        final ElementProperties[] ret = new ElementProperties[classNames.length];
        for (int i = 0; i < classNames.length; ++i) {
            ret[i] = map.get(classNames[i].trim());
        }

        return ret;
    }

    /**
     * 根据对齐的方式获取样式掩码。
     * <p>可选的对齐方式包括：</p>
     * <dl>
     * <dt>left</dt><dd>左对齐</dd>
     * <dt>right</dt><dd>右对齐</dd>
     * <dt>center</dt><dd>居中对齐</dd>
     * <dt>其它</dt><dd>无对齐</dd>
     * </dl>
     *
     * @param align
     *         对齐方式，自动去掉首尾空格并转成小写，如果是{@code null}则看作空字符串。
     * @return 对应的样式掩码。
     */
    private static int alignment(String align) {
        align = trimToEmpty(align).toLowerCase();
        switch (align) {
            case "left":
                return Element.ALIGN_LEFT;
            case "center":
                return Element.ALIGN_CENTER;
            case "right":
                return Element.ALIGN_RIGHT;
            default:
                return Element.ALIGN_UNDEFINED;
        }
    }

    /**
     * 根据RGB颜色，比如{@literal #ccddff}，创建{@link BaseColor}对象。
     *
     * @param color
     *         RGB颜色。
     * @return 对应的{@link BaseColor}对象。
     */
    private static BaseColor bColor(int color) {
        return new BaseColor(color);
    }

    /**
     * 判断字体的粗细。
     *
     * @param fontWeight
     *         字体粗细样式，自动去除首尾空格并转为小写，如果是{@code null}则看作空字符串。
     * @return 如果样式是{@literal bold}，那么字体是粗体，否则不是粗体。
     */
    private static boolean bold(String fontWeight) {
        return trimToEmpty(fontWeight).toLowerCase().equals("bold");
    }

    /**
     * 判断文本是否需要下划线。
     *
     * @param textDecoration
     *         文本下划线样式，自动去除首尾空格并转为小写，如果是{@code null}则看作空字符串。
     * @return 如果样式是{@literal underline}，那么文本需要加上下划线，否则不加。
     */
    private static boolean underline(String textDecoration) {
        return trimToEmpty(textDecoration).toLowerCase().equals("underline");
    }

    /**
     * 获取字体的样式。
     *
     * @param bold
     *         是否粗体。
     * @param italic
     *         是否斜体。
     * @return 字体的样式。
     */
    private static int style(
            boolean bold,
            boolean italic
    ) {
        return (bold ? Font.BOLD : 0) | (italic ? Font.ITALIC : 0);
    }

    /**
     * 为指定的文件创建URL。
     *
     * @param fileName
     *         文件名。
     * @return 对应的URL。
     * @throws IllegalArgumentException
     *         如果参数{@code fileName}是{@code null}或者只包含空白字符。
     * @throws FileNotFoundException
     *         如果指定的文件不存在。
     * @throws MalformedURLException
     *         指定的文件无法被表示为URL形式。
     */
    private static URL getFileURL(
            String fileName
    )
            throws FileNotFoundException, MalformedURLException {
        fileName = rejectIfBlank(fileName, "fileName").trim();

        if (startsWithIgnoreCase(fileName, CLASS_PATH_PREFIX)) {
            final String resourceName = fileName.substring(CLASS_PATH_PREFIX.length());
            final URL url = Thread.currentThread().getContextClassLoader().getResource(resourceName);
            if (url == null) {
                throw new FileNotFoundException("cannot find resource " + fileName);
            }
            return url;
        } else {
            return new File(fileName).toURI().toURL();
        }
    }

    @Override
    public final String getTitle() {
        return this.title;
    }

    @Override
    public final String getAuthor() {
        return this.author;
    }

    @Override
    public final String getSubject() {
        return this.subject;
    }

    @Override
    public final String[] getKeywords() {
        return this.keywords;
    }

    @Override
    public void render(
            OutputStream out,
            TemplateContext context
    )
            throws IOException {
        rejectIfNull(out, "out");
        rejectIfNull(context, "context");

        final var pdfDoc = new Document();
        try {
            PdfWriter.getInstance(pdfDoc, out);

            pdfDoc.addAuthor(this.author);
            pdfDoc.addTitle(this.title);
            pdfDoc.addSubject(this.subject);
            pdfDoc.addKeywords(join(this.keywords, ", "));
            pdfDoc.addCreationDate();

            pdfDoc.open();

            final Rectangle defaultPageSize = pdfDoc.getPageSize();
            boolean resetPageSize = false;
            for (final org.xx.armory.doc.Block block : this.doc) {
                if (block instanceof org.xx.armory.doc.Paragraph) {
                    if (resetPageSize) {
                        pdfDoc.setPageSize(defaultPageSize);
                        pdfDoc.newPage();
                        resetPageSize = false;
                    }
                    pdfDoc.add(createPdfParagraph((org.xx.armory.doc.Paragraph) block, this.documentProperties, context));
                } else if (block instanceof org.xx.armory.doc.Table) {
                    if (resetPageSize) {
                        pdfDoc.setPageSize(defaultPageSize);
                        pdfDoc.newPage();
                        resetPageSize = false;
                    }
                    pdfDoc.add(createPdfTable((org.xx.armory.doc.Table) block, this.documentProperties, context));
                } else if (block instanceof org.xx.armory.doc.FullImg) {
                    final Image fullImg = createFullImg((org.xx.armory.doc.FullImg) block, context);
                    if (fullImg != null) {
                        final float imgWidth = fullImg.getWidth();
                        final float imgHeight = fullImg.getHeight();
                        pdfDoc.setPageSize(new Rectangle(imgWidth + 8 * 2, imgHeight + 8 * 2));

                        pdfDoc.newPage();
                        fullImg.setAbsolutePosition(8, 8);
                        pdfDoc.add(fullImg);
                        resetPageSize = true;
                    }
                } else if (block instanceof org.xx.armory.doc.PageBreaker) {
                    if (((PageBreaker) block).isLandscape()) {
                        pdfDoc.setPageSize(defaultPageSize.rotate());
                    } else {
                        pdfDoc.setPageSize(defaultPageSize);
                    }
                    pdfDoc.newPage();
                    resetPageSize = false;
                } else {
                    throw new IllegalStateException("Unknown block: " + block);
                }
            }

            pdfDoc.close();
        } catch (DocumentException ex) {
            throw new IOException(ex);
        }
    }

    private void createBreaker(
            Paragraph p
    )
            throws DocumentException, IOException {
        p.add(Chunk.NEWLINE);
    }

    private void createText(
            Paragraph p,
            Text t,
            ElementProperties properties,
            TemplateContext context,
            BaseFont bf
    )
            throws DocumentException, IOException {
        final var ep = extend(properties, props(this.textProperties, t.getClassName()));

        final var fontSize = ep.getFloat(FONT_SIZE);
        final var font = new Font(bf, fontSize, style(bold(ep.getString(FONT_WEIGHT)), false), bColor(ep.getInteger(COLOR)));

        final var text = t.getText();

        final var itext = interpolate(context, text);

        if (t instanceof HyperLink) {
            final var url = ((HyperLink) t).getUrl();
            final var a = new Anchor(itext);
            a.setReference(url);
            p.add(a);
        } else {
            if (itext.equals("\u000c")) {
                // html 页面中需要插入字符实体&#12;
                p.add(Chunk.NEWPAGE);
            } else {
                final var c = new Chunk(itext, font);
                if (underline(ep.getString(TEXT_DECORATION))) {
                    c.setUnderline(.5F, fontSize * -.25F);
                }
                p.add(c);
            }
        }
    }

    private String interpolate(
            TemplateContext context,
            String text
    ) {
        final var buf = new StringBuilder();
        final var mo = PLACE_HOLDER.matcher(text);
        int pos = 0;

        while (mo.find()) {
            buf.append(text, pos, mo.start());

            final var key = mo.group(1);
            final var value = context.get(key);

            if (value != null && value.getClass().isArray()) {
                final var al = Array.getLength(value);
                if (al > 0) {
                    if (al > this.maxRepeatCount) {
                        this.maxRepeatCount = al;
                    }
                    final var ai = Array.get(value, this.repeatCount % al);

                    buf.append(ai);
                }
            } else {
                buf.append(value == null ? ("${" + key + "}") : value);
            }

            // 将查找位置移动到最近一个匹配项的末尾。
            pos = mo.end(0);
        }

        if (pos != text.length()) {
            buf.append(text.substring(pos));
        }

        return buf.toString();
    }

    private void createImg(
            Paragraph p,
            Img img,
            ElementProperties properties,
            TemplateContext context
    )
            throws DocumentException, IOException {
        final Image result;
        final var src = img.getSrc();

        final var computedProperties = extend(properties, props(this.paragraphProperties, img.getClassName()));

        if (startsWithIgnoreCase(src, "data:")) {
            final var dataKey = src.substring("data:".length());
            if (isBlank(dataKey)) {
                logger.warn("Illegal img data key");
                return;
            }

            final var imgDataObj = context.get(dataKey);
            if (imgDataObj instanceof File) {
                result = Image.getInstance(((File) imgDataObj).toURI().toURL());
            } else if (imgDataObj instanceof byte[]) {
                result = Image.getInstance((byte[]) imgDataObj);
            } else {
                logger.warn("Illegal img data: key={},value={}", dataKey, imgDataObj);
                return;
            }
        } else {
            result = Image.getInstance(getFileURL(src));
        }

        logger.trace("Img class: {}", img.getClassName());

        final var width = computedProperties.getSize(WIDTH);
        final var height = computedProperties.getSize(HEIGHT);

        logger.trace("Img width: {}", width);
        logger.trace("Img height: {}", height);

        result.setAlignment(alignment(computedProperties.getString(TEXT_ALIGN)));
        result.setSpacingAfter(computedProperties.getFloat(MARGIN_BOTTOM));
        result.setSpacingBefore(computedProperties.getFloat(MARGIN_TOP));

        if (width.getType() == ElementSize.ElementSizeType.PIXEL &&
                height.getType() == ElementSize.ElementSizeType.PIXEL) {
            result.scaleAbsolute(width.getValue(), height.getValue());
        } else {
            result.setScaleToFitLineWhenOverflow(true);
        }

        p.add(result);
    }

    private Image createFullImg(
            org.xx.armory.doc.FullImg img,
            TemplateContext context
    )
            throws DocumentException, IOException {
        final var src = img.getSrc();

        if (startsWithIgnoreCase(src, "data:")) {
            final var dataKey = src.substring("data:".length());
            if (isBlank(dataKey)) {
                logger.warn("Illegal img data key");
                return null;
            }

            final var imgDataObj = context.get(dataKey);
            if (imgDataObj instanceof File) {
                return Image.getInstance(((File) imgDataObj).toURI().toURL());
            } else if (imgDataObj instanceof byte[]) {
                return Image.getInstance((byte[]) imgDataObj);
            } else {
                logger.warn("Illegal img data: key={},value={}", dataKey, imgDataObj);
                return null;
            }
        } else {
            return Image.getInstance(getFileURL(src));
        }
    }

    /**
     * 根据文档段落创建一个PDF段落。
     *
     * @param paragraph
     *         文档段落。
     * @param properties
     *         段落属性。
     * @param context
     *         模板上下文。
     * @return 已创建的PDF段落。
     * @throws IllegalArgumentException
     *         如果参数{@code paragraph}或者{@code properties}或者{@code context}是{@code null}。
     * @throws DocumentException
     *         如果创建PDF段落出错。
     * @throws IOException
     *         如果创建段落字体时找不到指定的字体文件。
     */
    private Paragraph createPdfParagraph(
            org.xx.armory.doc.Paragraph paragraph,
            ElementProperties properties,
            TemplateContext context
    )
            throws DocumentException, IOException {
        rejectIfNull(paragraph, "paragraph");
        rejectIfNull(properties, "properties");
        rejectIfNull(context, "context");

        final var result = new Paragraph();

        final var computedProperties = extend(properties, props(this.paragraphProperties, paragraph.getClassName()));

        result.setAlignment(alignment(computedProperties.getString(TEXT_ALIGN)));
        result.setFirstLineIndent(computedProperties.getFloat(TEXT_INDENT));
        result.setIndentationLeft(computedProperties.getFloat(MARGIN_LEFT));
        result.setIndentationRight(computedProperties.getFloat(MARGIN_RIGHT));
        result.setSpacingAfter(computedProperties.getFloat(MARGIN_BOTTOM));
        result.setSpacingBefore(computedProperties.getFloat(MARGIN_TOP));
        result.setLeading(computedProperties.getFloat(FONT_SIZE) * 1.5F);

        final var bf = createFont(computedProperties.getString(FONT_FAMILY), IDENTITY_H, BaseFont.EMBEDDED);

        for (final Inline e : paragraph) {
            if (e instanceof Breaker) {
                createBreaker(result);
            } else if (e instanceof Text) {
                createText(result, (Text) e, computedProperties, context, bf);
            } else if (e instanceof Img) {
                createImg(result, (Img) e, computedProperties, context);
            } else {
                logger.warn("Cannot add {} to paragraph", e);
            }
        }

        return result;
    }

    /**
     * 根据文档表格创建一个PDF表格。
     *
     * @param table
     *         文档表格。
     * @param properties
     *         表格属性。
     * @param context
     *         模板上下文。
     * @return 已创建的PDF表格。
     * @throws IllegalArgumentException
     *         如果参数{@code table}或者{@code properties}或者{@code context}是{@code null}。
     * @throws DocumentException
     *         如果创建PDF表格出错。
     * @throws IOException
     *         如果创建表格字体时找不到指定的字体文件。
     */
    private PdfPTable createPdfTable(
            org.xx.armory.doc.Table table,
            ElementProperties properties,
            TemplateContext context
    )
            throws DocumentException, IOException {
        final PdfPTable ret;
        final int colsCount = table.getColumnsCount();

        if (table.isEmpty()) {
            ret = new PdfPTable(1);
        } else {
            ret = new PdfPTable(colsCount);
        }

        ret.setWidthPercentage(100);

        if (colsCount == 1) {
            ret.setWidths(new int[]{1});
        } else {
            final int[] columnWidths = new int[colsCount];
            for (int i = 0; i < colsCount; ++i) {
                columnWidths[i] = table.get(0).get(i).getWidth();
            }
            ret.setWidths(columnWidths);
        }

        final ElementProperties tp = ElementProperties.extend(properties, props(this.tableProperties, table.getClassName()));

        ret.setHorizontalAlignment(alignment(tp.getString(ALIGN)));
        ret.setSpacingAfter(tp.getFloat(MARGIN_BOTTOM));
        ret.setSpacingBefore(tp.getFloat(MARGIN_TOP));

        for (final org.xx.armory.doc.TableRow row : table) {
            this.repeatCount = 0;
            this.maxRepeatCount = 0;
            do {
                for (final org.xx.armory.doc.TableCell cell : row) {
                    final PdfPCell pc = new PdfPCell();
                    pc.setHorizontalAlignment(Element.ALIGN_CENTER);
                    pc.setVerticalAlignment(Element.ALIGN_CENTER);
                    pc.setPaddingTop(0);
                    pc.setPaddingBottom(5);
                    pc.setNoWrap(false);

                    for (final org.xx.armory.doc.Paragraph p : cell) {
                        pc.addElement(createPdfParagraph(p, properties, context));
                    }

                    pc.setColspan(cell.getColSpan());
                    pc.setRowspan(cell.getRowSpan());

                    ret.addCell(pc);
                }
                ++this.repeatCount;
            } while (this.repeatCount < this.maxRepeatCount);
        }

        return ret;
    }

    @Override
    public void close()
            throws Exception {
    }
}
